Estilo visual personalizado

A partir de v5.0 el estilo visual por defecto está definido en /web/naviox/style/naviox.css. Es parecido a esto:
naviox-customer-list_es.png
El archivo naviox.css está en tu proyecto pero se copia desde el proyecto Addons al desplegar. En todo caso, en lugar de modificar naviox.css es mejor modificar custom.css, como se explica en la siguiente sección.

Modificar el estilo por defecto (nuevo en v4.5)

Desde la versión 4.5 podemos añadir y modificar estilos mediante el fichero custom.css en web/xava/style.
Para sobreescribir un estilo ya existente, basta con añadir el nombre del estilo y definirlo en nuestro fichero. Poniendo:
body {
   font-size: 9px;
}
.ox-list-pair, .ox-list-odd {
   height: 20px;
}
Cambiamos el tamaño de la letra de toda nuestra aplicación (incluso dentro de un portal) y la altura de las filas en la lista. Mira en naviox.css para ver todas las classes CSS disponibles.

Añadir elementos a la página (nuevo en v5.6)

Si queremos añadir nuestros propios elementos a la pagina para que aparezcan siempre visibles en nuestra aplicación podemos hacerlo en web/naviox/indexExt.jsp de nuestro proyecto. Esto se incluye al final de la página, pero usando CSS podemos colocarlo en cualquier parte de la pantalla.
Por ejemplo, si queremos poner un cartelito de BETA en la esquina superior derecha de la pantalla, así:
beta-corner_es.png
Podemos poner en nuestro indexExt.jsp:
<div id="beta">BETA</div>
Y después posicionarlo añadiendo lo siguiente a custom.css:
#beta {
    position:absolute;
    right: -65px;
    top: 5px;
    color: white;
    font-size: 120%;
    font-weight: bold;
    transform: rotate(45deg);
    -webkit-transform: rotate(45deg);
    background: red;
    padding: 50px 50px 5px 50px;
    z-index: -10;
}

Dentro de portales

Dentro de un portal los portlets de OpenXava adquieren automáticamente la apariencia del portal, por lo tanto si trabajas con un portal simplemente has de crear o modificar el estilo del propio portal, y la aplicación de OpenXava se adaptará. Por ejemplo, cuando despliegas tu aplicación en Liferay se ve de esta forma:
liferay6-customer-list_es.png

Adicionalmente, si quieres un estilo específico para un portal, las siguientes propiedades están disponible en xava.properties: liferay6StyleClass, liferay51StyleClass, liferay41StyleClass, liferay43StyleClass, webSpherePortal61StyleClass, webSpherePortal6StyleClass, webSpherePortalStyleClass, JetSpeed2Style.
Así, si quieres crear tu propia estilo para las aplicaciones OpenXava cuando éstas ruedan dentro de un Liferary 6, añade la siguiente línea a xava.properties:
liferay6StyleClass=org.openxava.test.style.MyLiferay6Style
Y por supuesto, tendrás que escribir la clase MyLiferayStyle.

Definir tu propio estilo (hasta v4.9.1)

Estas instrucciones son para v4.2 a v4.9.1, para versiones anteriores consultar OpenXavaTest/xava.properties. Para v5.0 o superior usa default.css como se explica arriba.
Para definir tu propio estilo añade la siguiente entrada al archivo xava.properties de tu proyecto:
styleClass=org.openxava.web.style.Style
styleCSS=mystyle/mystyle.css
Después crea una carpeta llamada mystyle dentro de la carpeta web/xava/style, y añádele un archivo llamado mystyle.css. En este archivo puedes usar las siguientes clases CSS: ox-module, ox-view, ox-detail, ox-action-link, ox-image-link, ox-button-bar, ox-button-bar-button, ox-active, ox-first, ox-last, ox-label, ox-module-description, ox-frame-content, ox-list, ox-list-header, ox-list-subheader, ox-list-pair, ox-list-odd, list-pair-selected, list-odd-selected, ox-list-info, ox-list-info-detail, ox-list-title, ox-header-list-count, ox-frame (nuevo en v4.8, antes era frame), ox-frame-actions, ox-frame-title, ox-frame-title-label, ox-errors, ox-messages, ox-section, ox-section-tab, ox-active-section, ox-section-link, ox-collection-list-actions, ox-bottom-buttons, ox-page-navigation-selected, ox-page-navigation, ox-page-navigation-pages, ox-page-navigation-arrow, ox-page-navigation-arrow-disable, ox-images-gallery (new in v4.8), ox-help, ox-total-row, ox-total-cell, ox-total-capable-cell, ox-filter-cell, ox-frame-totals, ox-frame-totals-label, ox-frame-totals-value.
Opcionalmente, puedes crear tu propia clase Style extendiendo org.openxava.web.style.Style. Esto te permite control adicional sobre la apariencia, disposición y comportamiento de tu aplicación.

Estilos predefinidos (hasta v4.9.1)

Puedes usar los estilos en esta sección con v5.0 si desactivas los menús y la identificación de usuario como se explica en la migración de v4.9.1 a v5.0.
OpenXava viene con algunos estilos predefinidos incluidos. Los puedes configurar en properties/xava.properties de tu proyecto:
# styleClass and styleCSS have only effect outside a portal,
# inside a portal OX portlets acquire automatically the portal skin.
 
# Liferay 5.1: This is the DEFAULT, so you do not need to uncomment the next lines
#styleClass=org.openxava.web.style.Liferay51Style
#styleCSS=liferay51/css/everything_unpacked.css
 
# Liferay 4.3/4.4/5.0: uncomment the next line for use this nice and slow style
#styleClass=org.openxava.web.style.Liferay43Style
#styleCSS=liferay43/css/everything_unpacked.css
 
# Classic style: uncomment the next line for use the old OX style
#styleClass=org.openxava.web.style.PortalStyle
#styleCSS=default.css

Un ejemplo: estilo para iPad (hasta v4.9.1)

Con v5.0 el estilo estándar funciona bien con tablets, de todas formas puedes usar el estilo antiguo de iPad si desactivas los menús y la identificación de usuario como se explica en la migración de v4.9.1 a v5.0. Además, a partir de v5.1 deberías añadir la entrada org.openxava.web.style.IPadStyle en styles.properties.
El estilo por iPad se incluye por defecto en OpenXava y se activa automáticamente en el momento en que un usuario accede a la aplicación desde un iPad.

Detección de navegador/dispositivo

Para conseguir la detección del dispositivo, añadimos el siguiente método a la clase de estilo:
public boolean isForBrowse(String browser) {
    return browser != null && browser.contains("iPad");
}
Esta es la forma para hacer corresponder un estilo específico con un navegador o dispositivo. Puedes usar esta técnica en tu estilo, aunque si lo haces, has de crear un archivo styles.properties en la carpeta properties de tu proyecto, añadiendo una lína con la clase de tu estilo, como la siguiente:
com.mycompany.myapp.style.MyDeviceStyle
Además, has de escribir la clase MyDeviceStyle con le método isForBrowse() apropiado.

Código JavaScript

El estilo para iPad también usa efectos nativos para iPad como flip (rotación) y slide (desplazamiento). Para conseguir esto usamos algo de código JavaScript. En este caso definimos nuestro propio método para especificar la forma en que un trozo de HTML obtenido del servidor vía AJAX se asigna a un elemento del documento HTML:
public String getSetHtmlFunction() {
    return "ipad.setHtml";
}
De esta forma, indicamos que la función a usar será ipad.setHtml, en vez de la estándar. Esta función está definida en el fichero ipad.js. Tenemos que indicar en la clase de estilo que este fichero se ha de cargar:
private static String [] jsFiles = {
    "ipad/ipad.js",
};
 
public String [] getNoPortalModuleJsFiles() {
    return jsFiles;
}
Este archivo ipad.js se encuentra en la carpeta web/style/ipad:
if (ipad == null) var ipad = {};
 
ipad.onLoad = function() {
    var id = $('body').data('ipad-slide-element');
    var clazz = $('body').data('ipad-slide-class');
    $('#' + id).addClass(clazz);
    setTimeout("$('#" + id + "').removeClass('" + clazz + "')", 0);
    $('body').removeData();
}
 
ipad.onClickNextPage = function(listId) {
    $('#' + listId).addClass('slided-left');
    $('body').data('ipad-slide-element', listId);
    $('body').data('ipad-slide-class', 'slided-right');
}
 
ipad.onClickPreviousPage = function(listId) {
    $('#' + listId).addClass('slided-right');
    $('body').data('ipad-slide-element', listId);
    $('body').data('ipad-slide-class', 'slided-left');
}
 
ipad.setHtml = function(id, content) {
    if (!id.match(/__core$/)) {
        $("#" + id).html(content);
        return;
    }
 
    // Core
    $("#container").addClass("container");
    $("#sheet").addClass("sheet");
    $("#front").addClass("face");
    $("#back").addClass("face");
 
    if ($("#sheet").hasClass("flipped")) {
        $("#" + id).html(content);
        $("#sheet").removeClass("flipped");
    }
    else {
        $("#core_BACK").html(content);
        $("#sheet").addClass("flipped");
    }
 
    $('#sheet').bind('webkitTransitionEnd',
        function( event ) {
            if ($("#sheet").hasClass("flipped")) $("#" + id).empty();
            else {
                $("#core_BACK").empty();
                $("#container").removeClass("container");
                $("#sheet").removeClass("sheet");
                $("#front").removeClass("face");
                $("#back").removeClass("face");
            }
            $('#sheet').unbind();
        },
        false
    );
 
}

Archivo CSS

Aquí tenemos el ipad.css completo. Lo puedes encontrar en la distribución de OpenXava, en la carpeta OpenXava/web/style/ipad.
body {
    margin: 0px; background: black;
}
 
#xava_loading, #xava_loading2 {
    padding: 20px;
        border: none;
    background: -webkit-gradient(linear, left bottom, left top,
        from(#10182C), color-stop(0.5, #121A2E),
        color-stop(0.51, #282F41), to(#7E838D));
    opacity:.60;
    -webkit-border-radius: 7px;
    color: white;
    text-shadow:0px -1px 1px rgba(0,0,0,0.75);
    font-weight: bold;
    -webkit-box-shadow: 10px 10px 10px rgba(140,140,140,0.75);;
    display:none;
    z-index: 9999;
    position: fixed;
    left: 45%
}
 
#xava_loading {
    top: 25%;
}
 
#xava_loading2 {
    top: 120%;
}
 
.ox-module {
    font-family: Helvetica, Arial, sans-serif;
}
 
 
/* Portrait */
@media screen and (orientation:portrait) {
    .ox-module, .ui-widget-content, #xava_loading, #xava_loading2 {
        font-size: 1.2em;
    }
    .ui-widget-content {
        font-size: 1.1em;
    }
    .ui-widget-header {
        font-size: 180%;
    }
    .ox-button-bar, .ox-section {
        height: 32px;
    }
    .ox-page-navigation-pages {
        top: 8px;
    }
    .ox-bottom-buttons input[type="button"] {
        -webkit-box-shadow: 0px 2px 0px white;
    }
    .ox-detail, .ox-frame-content, .ox-active-section {
        border: 2px solid #B2B4B8;
        -webkit-box-shadow: 0px 2px 0px white;
    }
    .ox-frame-actions img {
        height: 38px;
    }
}
 
 
 
/* Landscape */
@media screen and (orientation:landscape) {
    .ox-module {
        font-size: 0.9em;
    }
    .ui-widget-content, #xava_loading, #xava_loading2 {
        font-size: 0.95em;
    }
    .ui-widget-header {
        font-size: 150%;
    }
    .ox-button-bar, .ox-section {
        border-bottom: none;
        height: 25px;
    }
    .ox-page-navigation-pages {
        top: 11px;
    }
    .ox-bottom-buttons input[type="button"] {
        -webkit-box-shadow: 0px 1px 0px white;
    }
    .ox-button-bar-image img {
        height: 19px;
    }
    .ox-detail, .ox-frame-content, .ox-active-section {
        border: 1px solid #B2B4B8;
        border-top: 1px solid #8F8F8F;
        -webkit-box-shadow: 0px 1px 0px white;
    }
}
 
.ox-view {
    background: #D8D8D9;
    padding: 20px;
    border-bottom: 1px solid #959596;
}
 
.ox-detail {
    font-size: 130%;
 
}
 
.ox-detail, .ox-frame-content, .ox-active-section {
    background: white;
    padding: 10px;
    -webkit-border-radius:15px;
    color: #385587;
    text-shadow: 0px 1px 1px black #A0ADC4;
}
 
.ox-active-section {
    -webkit-border-top-left-radius: 0px;
    -webkit-border-top-right-radius: 0px;
    border-left: 1px solid #B2B4B8;
    border-right: 1px solid #B2B4B8;
}
 
.ox-frame-title-label {
    padding: 10px 0px 0px 10px;
    vertical-align: bottom;
}
 
input {
    color: #385587;
}
 
.ox-label {
    text-align: left;
    color: black;
    font-weight: bold;
    text-shadow: 0px 2px 1px #E8E8E9;
}
 
.ox-button-bar, .ox-collection-list-actions, .ox-section {
    font-size: 0.9em;
    padding: 10px 4px 0px 6px;
    border-top: 1px solid white;
    -webkit-border-top-left-radius: 7px;
    -webkit-border-top-right-radius: 7px;
    background: white;
    background: -webkit-gradient(linear, left bottom, left top,
        from(#A8ADB9), to(#F5F6F7));
}
.ox-collection-list-actions {
    padding: 0px 4px 0px 6px;
    height: 40px;
}
 
.ox-detail .ox-action-link {
    background: white;
    padding: 5px;
    border: 1px solid #C4C4C4;
    -webkit-border-radius:5px;
    color: #7C7A77;
    font-size: 0.9em;
    text-decoration: none;
    text-shadow:1px 0px 0px #EDEDED;
}
 
.ox-button-bar-button a, .ox-list .ox-action-link {
    margin-right: 5px;
    padding: 4px 9px 5px;
    border: 1px solid #51555B;
    border-top: 1px solid #4C4E50;
    -webkit-border-radius:5px;
    background: -webkit-gradient(linear, left bottom, left top,
        from(#6B737E), to(#B2B5BB));
    color: white;
    font-weight: bold;
    text-decoration:none;
    text-shadow:0px -1px 1px rgba(0,0,0,0.75);
}
 
.ox-button-bar-image {
    padding: 0px 15px 5px;
    vertical-align: top;
}
 
.ox-button-bar-mode-button, .ox-section-tab, .ox-section-link {
    padding: 4px 9px 5px;
    border: 1px solid #51555B;
    border-right: none;
    border-left: none;
    border-top: 1px solid #4C4E50;
    background: -webkit-gradient(linear, left bottom, left top,
        from(#A8ADB9), to(#F5F6F7));
    font-weight: bold;
    text-decoration:none;
    color: #5D6065;
    text-shadow:0px 1px 1px white;
}
 
.ox-active .ox-button-bar-mode-button, .ox-active .ox-section-tab {
    background: -webkit-gradient(linear, left bottom, left top,
        from(#6B737E), to(#B2B5BB));
    color: white;
    text-shadow:0px -1px 1px rgba(0,0,0,0.75);
}
 
.ox-first .ox-button-bar-mode-button, .ox-first .ox-section-tab {
    border-left: 1px solid #51555B;
    -webkit-border-top-left-radius: 5px;
    -webkit-border-bottom-left-radius: 5px;
}
 
.ox-last .ox-button-bar-mode-button, .ox-last .ox-section-tab {
    border-right: 1px solid #51555B;
    -webkit-border-top-right-radius: 5px;
    -webkit-border-bottom-right-radius: 5px;
}
 
.ox-list {
    font-size: 0.9em;
    border: none;
    padding: 2px;
    -webkit-border-radius:7px;
    -webkit-box-shadow: inset 1px 1px 4px #4E4E4E;
    color: #5D6065;
    font-weight: bold;
    text-shadow:0px 1px 1px white;
    background: #D8D8D9;
    -webkit-transition-property: -webkit-transform;
    -webkit-transition-duration: 1.5s;
}
 
.slided-left {
    -webkit-transform: translateX(-130%);
}
 
.slided-right {
    -webkit-transform: translateX(200%);
}
 
 
.ox-list-odd {
    background: #BBBBBB;
    border-top: 1px solid #CFCFCF;
    border-bottom: 1px solid #959596;
    padding: 10px 8px 10px 8px;
}
 
.ox-list-pair {
    background: #AFAFAF;
    border-top: 1px solid #C6C6C6;
    border-bottom: 1px solid #959596;
    padding: 10px 8px 10px 8px;
}
 
.ox-total-row {
    height: 40px;
    background: #AFAFAF;
    border-top: 1px solid #C6C6C6;
    border-bottom: 1px solid #959596;
    background: -webkit-gradient(linear, left bottom, left top,
        from(#565A63), color-stop(0.5, #6A6E76),
        color-stop(0.51, #797D84), to(#91969E));
}
 
.ox-total-cell, .ox-total-capable-cell {
    text-align: right;
    color: white;
    text-shadow:0px -1px 1px rgba(0,0,0,0.75);
}
 
.ox-list-header a {
    color: white;
    text-shadow:0px -1px 1px rgba(0,0,0,0.75);
}
 
.ox-list-header {
    background: -webkit-gradient(linear, left bottom, left top,
        from(#797D84), to(#91969E));
}
 
.ox-list-subheader {
    background: -webkit-gradient(linear, left bottom, left top,
        from(#565A63), to(#6A6E76));
    padding: 2px 8px 2px 10px;
}
 
.ox-list-title, .ox-module-description {
    font-weight: bold;
    font-size: 140%;
    color: #2C2F33;
    text-shadow:0px 2px 1px #E8E8E9;
    padding-left: 12px;
    padding-bottom: 12px;
}
 
.ox-module-description {
    color: #385587;
    text-shadow: 0px 1px 1px white;
 
}
 
.ox-header-list-count {
    margin-left: 10px;
    font-size: 75%;
}
 
.ox-list-info {
    margin-top: 10px;
    -webkit-border-radius:7px;
    border: 1px solid #505153;
    background: -webkit-gradient(linear, left bottom, left top,
        from(#565A63), color-stop(0.5, #6A6E76),
        color-stop(0.51, #797D84), to(#91969E));
}
 
.ox-page-navigation-arrow-disable, .ox-page-navigation-arrow img {
    padding: 5px 10px 7px 10px;
}
 
.ox-first .ox-page-navigation-arrow, .ox-first .ox-page-navigation-arrow-disable {
    border-right: 2px ridge #878B91;
    float: left;
}
 
.ox-last .ox-page-navigation-arrow, .ox-last .ox-page-navigation-arrow-disable {
    border-left: 2px groove #878B91;
    float: right;
}
 
.ox-page-navigation-pages {
    float:left;
    position:relative;
    left:40%;
}
 
.ox-page-navigation, .ox-page-navigation-selected {
    margin-right: 10px;
}
 
.ox-bottom-buttons {
    border-top: 1px solid white;
    background: #D8D8D9;
    padding: 10px;
    text-align: center;
}
 
.ox-bottom-buttons input[type="button"] {
    padding: 20px;
    margin: 10px;
    border: 1px solid #979797;
    -webkit-border-radius:10px;
    background: -webkit-gradient(linear, left bottom, left top,
        from(#D9D9D9), to(white));
    color: black;
    font-weight: bold;
    font-size: 110%;
}
 
.ox-errors {
    color: #CF0000;
    font-weight: bold;
    font-size: 140%;
    text-shadow:0px 2px 1px #E8E8E9;
}
 
.ox-messages {
    color: #2160DD;
    font-weight: bold;
    font-size: 140%;
    text-shadow:0px 2px 1px #E8E8E9;
}
 
.ox-filter-cell {
    text-align: left;
}
 
.ox-frame-totals {
    float: right;
}
.ox-frame-totals-label {
    font-weight: normal;
}
 
.ox-frame-totals-value {
    font-weight: bold;
}
 
.ui-widget-header {
    border: none;
    background: -webkit-gradient(linear, left bottom, left top,
        from(#10182C), color-stop(0.5, #121A2E),
        color-stop(0.51, #282F41), to(#7E838D));
    color: white;
    text-shadow:0px -1px 1px rgba(0,0,0,0.75);
    font-weight: bold;
}
 
.ui-corner-all {
    -webkit-border-radius: 10px;
}
 
.ui-widget-content {
    border: none;
    background: #151D31;
    -webkit-box-shadow: 15px 15px 30px rgba(140,140,140,0.75);;
}
 
.ui-widget-content .ox-view {
    -webkit-border-top-left-radius: 7px;
    -webkit-border-top-right-radius: 7px;
}
.ui-widget-content .ox-bottom-buttons {
    -webkit-border-bottom-left-radius: 7px;
    -webkit-border-bottom-right-radius: 7px;
}
 
.ui-dialog .ui-dialog-content {
    padding: 5px;
}
 
 
/* For flip effect */
.container {
    -webkit-perspective: 600;
}
 
 
.sheet {
    position: absolute;
     width: 100%;
}
 
 
.face {
    position: absolute;
    width: 100%;
    overflow: hidden;
}
 
 
.sheet {
    -webkit-transform-style: preserve-3d;
    -webkit-transition-property: -webkit-transform;
    -webkit-transition-duration: 1.5s;
}
 
 
.flipped
{
    -webkit-transform: rotateY(180deg);
}
 
#front, #back {
    -webkit-backface-visibility: hidden;
}
 
#back {
    -webkit-transform: rotateY(180deg);
}

Clase de estilo

Aquí está el código completo para la clase IPadStyle. Puedes encontrar esta clase en la distribución de OpenXava, en OpenXava/src/org.openxava.web.style.
package org.openxava.web.style;
 
/**
 * For iPads. <p>
 *
 * @author Javier Paniza
 */
 
public class IPadStyle extends Style {
 
    private static String [] jsFiles = {
        "ipad/ipad.js",
    };
 
    private static IPadStyle instance = null;
 
    protected IPadStyle() {
    }
 
    public static Style getInstance() {
        if (instance == null) {
            instance = new IPadStyle();
        }
        return instance;
    }
 
    public boolean isForBrowse(String browser) {
        return browser != null && browser.contains("iPad");
    }
 
    public boolean isOnlyOneButtonForModeIfTwoModes() {
        return true;
    }
 
    public boolean isSeparatorBeforeBottomButtons() {
        return false;
    }
 
    public String getSetHtmlFunction() {
        return "ipad.setHtml";
    }
 
 
    public String getCssFile() {
        return "ipad/ipad.css";
    }
 
    public String [] getNoPortalModuleJsFiles() {
        return jsFiles;
    }
 
 
    public String getInitThemeScript() {
        return "ipad.onLoad()";
    }
 
    public String getNextPageNavigationEvents(String listId) {
        return "onclick=ipad.onClickNextPage('" + listId + "')";
    }
 
    public String getPreviousPageNavigationEvents(String listId) {
        return "onclick=ipad.onClickPreviousPage('" + listId + "')";
    }
 
    public String getCoreStartDecoration() {
        return "<div id='container'><div id='sheet'><div id='front'>";
    }
 
    public String getCoreEndDecoration() {
        return
            "</div><div id='back'><div id='core_BACK' style='display: inline;' class='"
            + getModule() + "'></div></div></div></div>";
    }
 
    public String getDefaultModeController() {
        return "DetailList";
    }
 
    public boolean allowsResizeColumns() {
        return false;
    }
 
    public boolean isRowLinkable() {
        return false;
    }
 
    public boolean isShowPageNumber() {
        return false;
    }
 
    public boolean isShowModuleDescription() {
        return true;
    }
 
    public boolean isSeveralActionsPerRow() {
        return false;
    }
 
    public boolean isChangingPageRowCountAllowed() {
        return false;
    }
 
    public boolean isHideRowsAllowed() {
        return false;
    }
 
    public boolean isShowRowCountOnTop() {
        return true;
    }
 
    public boolean isUseLinkForNoButtonBarAction() {
        return true;
    }
 
    public boolean isHelpAvailable()  {
        return false;
    }
 
    public boolean isShowImageInButtonBarButton() {
        return false;
    }
 
    public boolean isUseStandardImageActionForOnlyImageActionOnButtonBar() {
        return true;
    }
 
    public boolean isFixedPositionSupported() {
        return false;
    }
 
 
    public String getMetaTags() {
        return "<meta name='apple-mobile-web-app-capable' content='yes'/>";
    }
 
 
    public String getModuleSpacing() {
        return "style='padding: 0px;'";
    }
 
    public String getListCellSpacing() {
        return "cellspacing=0 cellpadding=0";
    }
 
    public String getImagesFolder() {
        return "xava/style/ipad/images";
    }
 
}